package com.bkhech.spring.security.example.config;

import cn.hutool.core.collection.CollUtil;
import com.bkhech.spring.security.example.accesstoken.oauth2.OAuth2TokenApi;
import com.bkhech.spring.security.example.authority.SecurityFrameworkService;
import com.bkhech.spring.security.example.authority.SecurityFrameworkServiceImpl;
import com.bkhech.spring.security.example.filter.TokenAuthenticationFilter;
import com.bkhech.spring.security.example.handler.CustomizeAccessDeniedHandlerImpl;
import com.bkhech.spring.security.example.handler.CustomizeAuthenticationEntryPointImpl;
import com.bkhech.spring.security.example.handler.CustomizeAuthenticationSuccessHandler;
import com.bkhech.spring.security.example.permission.PermissionApi;
import com.bkhech.spring.security.example.service.impl.UserServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.security.PermitAll;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 *
 * {@link @EnableGlobalMethodSecurity} : 开启 Spring Security 方法级安全注解
 *
 * prePostEnabled :决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]
 * secureEnabled : 决定是否Spring Security的保障注解 [@Secured] 是否可用
 * jsr250Enabled ：决定 JSR-250 annotations 注解[@RolesAllowed..] 是否可用.
 *
 *
 * @author guowm
 * @date 2023/1/10
 */

@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class WebSecurityConfigurerAdapter {
    private final SecurityProperties securityProperties;

    private final OAuth2TokenApi oAuth2TokenApi;

    private final ApplicationContext applicationContext;

    /**
     * @return
     * @see UserServiceImpl
     */
//    @Bean
//    public UserDetailsService userDetailsService() {
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        manager.createUser(User.withUsername("zhangsan").password("123456").authorities("p1").build());
//        manager.createUser(User.withUsername("lisi").password("123456").authorities("p2").build());
//        return manager;
//    }
//    @Bean
//    public PasswordEncoder passwordEncoder() {
//        return NoOpPasswordEncoder.getInstance();
//    }

    /**
     * Spring Security 加密器
     * 考虑到安全性，这里采用 BCryptPasswordEncoder 加密器
     *
     * @see <a href="http://stackabuse.com/password-encoding-with-spring-security/">Password Encoding with Spring Security</a>
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证失败处理类 Bean
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new CustomizeAuthenticationEntryPointImpl();
    }

    /**
     * 权限不够处理器 Bean
     */
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new CustomizeAccessDeniedHandlerImpl();
    }

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new CustomizeAuthenticationSuccessHandler();
    }

    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new AuthenticationEntryPointFailureHandler(authenticationEntryPoint());
    }

    @Bean
    public TokenAuthenticationFilter authenticationFilter(OAuth2TokenApi oAuth2TokenApi) {
        return new TokenAuthenticationFilter(securityProperties, oAuth2TokenApi);
    }

    /**
     * 使用 Spring Security 的缩写，方便使用
     * 自定义权限框架别名
     */
    @Bean("ss")
    public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) {
        return new SecurityFrameworkServiceImpl(permissionApi);
    }

    /**
     * 静态资源设置
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring()
                .antMatchers(
                        "/",
                        "/favicon.ico"
                );
    }

    /**
     * http请求设置
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 开启跨域
                .cors().and()
                // formLogin: 开启表单登录配置
                // loginPage: 配置登录页面地址, 默认是自动生成的界面
                // successHandler(): 登录成功之后的处理器
//                 .formLogin().loginProcessingUrl("/login").successHandler(authenticationSuccessHandler()).failureHandler(authenticationFailureHandler()).and()

                // 基于 token 机制，所以不需要 Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                //解决 in a frame because it set 'X-Frame-Options' to 'DENY' 问题
                .headers().frameOptions().disable().and()
                // CSRF 禁用，因为不使用 Session
                .csrf().disable()
                // 一堆自定义的 Spring Security 处理器
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint()).accessDeniedHandler(accessDeniedHandler());

        // 登录暂时不使用 Spring Security 的拓展点，主要考虑一方面拓展多用户、多种登录方式相对复杂，一方面用户的学习成本较高

        // 获得 @PermitAll 带来的 URL 列表，免登录
        final MultiValueMap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations();
        // 设置每个请求的权限
        httpSecurity
                // 1 全局共享规则
                // 表示开启权限配置
                .authorizeRequests()
                // 1.0 测试：permitAll() 表示请求(用户)可以任意访问，/hello测试地址
                .antMatchers("/hello").permitAll()
                // 1.1 静态资源可匿名访问
                .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
                // 1.2 设置 @PermitAll 无需认证
                .antMatchers(HttpMethod.GET, permitAllUrls.getOrDefault(HttpMethod.GET, List.of()).toArray(new String[0])).permitAll()
                .antMatchers(HttpMethod.POST, permitAllUrls.getOrDefault(HttpMethod.POST, List.of()).toArray(new String[0])).permitAll()
                .antMatchers(HttpMethod.PUT, permitAllUrls.getOrDefault(HttpMethod.PUT, List.of()).toArray(new String[0])).permitAll()
                .antMatchers(HttpMethod.DELETE, permitAllUrls.getOrDefault(HttpMethod.DELETE, List.of()).toArray(new String[0])).permitAll()
                // 2 兜底认证规则
                // 表示其他任何尚未匹配的URL请求(用户)，要经过认证(用户登录)之后才能访问
                .anyRequest().authenticated();

        // 添加 Token Filter
        httpSecurity.addFilterBefore(authenticationFilter(oAuth2TokenApi), UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

    private MultiValueMap<HttpMethod, String> getPermitAllUrlsFromAnnotations() {
        MultiValueMap<HttpMethod, String> result = new LinkedMultiValueMap<>();
        // 获得接口对象的 HandlerMethod 集合
        final RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        final Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();

        // 获得有 @PermitAll 注解的接口
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
            final HandlerMethod handlerMethod = entry.getValue();
            if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) {
                continue;
            }
            final RequestMappingInfo requestMappingInfo = entry.getKey();
            if (requestMappingInfo.getPathPatternsCondition() == null) {
                continue;
            }
            final Set<String> urls = requestMappingInfo.getPathPatternsCondition().getPatternValues();
            // 根据请求方法，添加到 result 结果
            requestMappingInfo.getMethodsCondition().getMethods().forEach(requestMethod -> {
                switch (requestMethod) {
                    case GET:
                        result.addAll(HttpMethod.GET, CollUtil.newArrayList(urls));
                        break;
                    case POST:
                        result.addAll(HttpMethod.POST, CollUtil.newArrayList(urls));
                        break;
                    case PUT:
                        result.addAll(HttpMethod.PUT, CollUtil.newArrayList(urls));
                        break;
                    case DELETE:
                        result.addAll(HttpMethod.DELETE, CollUtil.newArrayList(urls));
                        break;
                    default:
                }
            });
        }
        return result;
    }

}
